use base64::{engine::general_purpose::STANDARD as encoder, Engine as _};
use bytes::Buf;
use chrono::Utc;
use hmac::{Hmac, Mac};
use http_body_util::BodyExt;
use hyper::{Method, Request};
use hyper_tls::HttpsConnector;
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
use serde_json::Value;
use sha1::Sha1;
use std::collections::HashMap;
use std::error::Error;
use std::io::Read;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::runtime::Runtime;
use urlencoding::encode;
use uuid::Uuid;

type HmacSha1 = Hmac<Sha1>;
pub struct Aliapi {
    host: String,
    access_key: String,
    secret_key: String,
}

impl Aliapi {
    pub fn new(access_key: &str, secret_key: &str) -> Aliapi {
        Aliapi {
            host: String::from("https://alidns.aliyuncs.com/"),
            access_key: String::from(access_key),
            secret_key: String::from(secret_key),
        }
    }

    pub fn update_domain_record(
        &self,
        record_id: &str,
        local_ip: &str,
        sub_name: &str,
        record_type: &str,
    ) -> Result<Value, Box<dyn Error>> {
        let mut params: HashMap<&str, &str> = HashMap::new();
        params.insert("RR", sub_name);
        params.insert("RecordId", record_id);
        params.insert("Value", local_ip);
        params.insert("Type", record_type);
        return self.do_request("UpdateDomainRecord", params);
    }

    pub fn domain_records(
        &self,
        domain_name: &str,
        sub_name: &str,
    ) -> Result<Value, Box<dyn Error>> {
        let mut params: HashMap<&str, &str> = HashMap::new();
        params.insert("SearchMode", "EXACT");
        params.insert("DomainName", domain_name);
        params.insert("KeyWord", sub_name);
        return self.do_request("DescribeDomainRecords", params);
    }

    fn do_request(&self, api: &str, params: HashMap<&str, &str>) -> Result<Value, Box<dyn Error>> {
        let mut headers: HashMap<&str, &str> = HashMap::new();
        let mut query: HashMap<&str, &str> = HashMap::new();
        query.insert("SignatureVersion", "1.0");
        query.insert("Action", api);
        query.insert("Format", "json");
        query.insert("Version", "2015-01-09");
        query.insert("AccessKeyId", self.access_key.as_str());
        query.insert("SignatureMethod", "HMAC-SHA1");
        let nonce_str = Aliapi::nonce();
        query.insert("SignatureNonce", nonce_str.as_str());
        let timestamp = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();
        query.insert("Timestamp", timestamp.as_str());

        headers.insert("x-acs-action", api);
        headers.insert("host", "alidns.aliyuncs.com");
        headers.insert("content-type", "application/x-www-form-urlencoded");
        headers.insert("x-acs-version", "2015-01-09");
        headers.insert(
            "user-agent",
            "AlibabaCloud (Windows 10; amd64) Java/17.0.7+7 tea-util/0.2.6 TeaDSL/1",
        );

        let mut sign_map: HashMap<&str, &str> = HashMap::new();
        sign_map.extend(query.clone());
        sign_map.extend(params.clone());
        let signrst = Aliapi::signature(sign_map.clone(), "POST", self.secret_key.as_str());
        query.insert("Signature", &signrst);

        let records = self.post(query, params, headers);
        let json: Value = serde_json::from_str(records?.as_str())?;
        Ok(json)
    }

    fn post(
        &self,
        query: HashMap<&str, &str>,
        body: HashMap<&str, &str>,
        header: HashMap<&str, &str>,
    ) -> Result<String, Box<dyn Error>> {
        let mut builder = Request::builder();
        for (key, value) in header {
            builder = builder.header(key, value);
        }

        let mut query_str = String::new();
        for (key, value) in query {
            let v = encode(value);
            query_str = format!("{query_str}&{key}={v}");
        }
        query_str.remove(0);
        let url = format!("{0}?{query_str}", self.host);
        builder = builder.method(Method::POST).uri(url);

        let mut body_str = String::new();
        for (key, value) in body {
            let v = encode(value);
            body_str = format!("{body_str}&{key}={v}");
        }
        body_str.remove(0);
        let req = builder.body(body_str)?;

        let https = HttpsConnector::new();
        let client = Client::builder(TokioExecutor::new()).build::<_, String>(https);

        let rt = Runtime::new()?;
        let str = rt.block_on(async {
            let resp = client.request(req).await.unwrap();

            let body = resp.collect().await.unwrap().aggregate();
            let mut buffer = String::new();
            body.reader().read_to_string(&mut buffer).unwrap();
            buffer
        });

        Ok(str)
    }

    fn sign_encode(value: &str) -> String {
        return encode(value)
            .replace("+", "%20")
            .replace("*", "%2A")
            .replace("%7E", "~");
    }

    fn signature(params: HashMap<&str, &str>, method: &str, secret: &str) -> String {
        let mut sorted_key: Vec<_> = params.keys().into_iter().collect();
        sorted_key.sort();
        let mut signstr = String::new();
        for key in sorted_key {
            let value = params.get(key).unwrap();

            signstr = format!("{signstr}&{key}={}", Aliapi::sign_encode(value));
        }
        signstr.remove(0);
        let signstr = Aliapi::sign_encode(&signstr);
        let signstr = format!("{method}&%2F&{signstr}");

        let mut mac = HmacSha1::new_from_slice(format!("{secret}&").as_bytes())
            .expect("使用指定的 sk 初始化失败");
        mac.update(signstr.as_bytes());
        let result = mac.finalize();
        return encoder.encode(result.into_bytes());
    }

    fn nonce() -> String {
        let uid = Uuid::new_v4();
        let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
        let micios = time.as_millis();
        format!("{uid}{micios}1").to_string()
    }
}
